/**
 * Copyright (C) 2010 altuure <altuure [AT] gmail [DOT] com> http://www.altuure.com/projects/yagdao
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.altuure.yagdao.common;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Fast Property accessor for a single class.
 * @author altuure
 */

@SuppressWarnings("rawtypes")
public class FastPropertyUtil {
    public static final Log log = LogFactory.getLog(FastPropertyUtil.class);
    /**
     * init setter and getter lazy.
     */
    public static final boolean LAZY = true;

    /**
     * Get method prefix.
     */
    private static final String GET = "get";
    /**
     * is method prefix.
     */
    private static final String IS = "is";
    /**
     * set method prefix.
     */
    private static final String SET = "set";

    /**
     * set method cache.
     */
    private Map<String, NestedHandler> setters = new HashMap<String, NestedHandler>();

    /**
     * getter method cache.
     */
    private Map<String, NestedHandler> getters = new HashMap<String, NestedHandler>();

    /**
     * wrapped class .
     */
    private Class adaptedClass;

    /**
     * create new instance for given class.
     * @see #LAZY
     * @param adaptedClass wrapped class
     */
    public FastPropertyUtil(Class adaptedClass) {
        this(adaptedClass, LAZY);

    }

    /**
     * create new instance for given class .
     * @param adaptedClass wrapped class
     * @param initLazy true if init lazy or false init now
     */
    public FastPropertyUtil(Class adaptedClass, boolean initLazy) {
        super();
        this.adaptedClass = adaptedClass;
        init(initLazy);
    }

    /**
     * set object property value.
     * @param obj object
     * @param property property
     * @param value value
     * @throws IllegalArgumentException  underlying exception
     * @see {@link Method#invoke(Object, Object...)}
     * @throws IllegalAccessException underlying exception
     * @see {@link Method#invoke(Object, Object...)}
     * @throws InvocationTargetException underlying exception
     * @throws InstantiationException new instance error
     * @see {@link Method#invoke(Object, Object...)}
     */
    public void set(Object obj, String property, Object value) throws IllegalArgumentException, IllegalAccessException,
            InvocationTargetException, InstantiationException {
        if (setters.containsKey(property)) {
            NestedHandler member = setters.get(property);
            member.set(obj, value);
        } else {
            NestedHandler nestedHandler = new NestedHandler(property, true);
            setters.put(property, nestedHandler);
            nestedHandler.set(obj, value);
        }

    }

    /**
     * get object property value.
     * @param obj object
     * @param property property
     * @return value
     * @throws IllegalArgumentException underlying exception
     * @see {@link Method#invoke(Object, Object...)}
     * @throws IllegalAccessException    underlying exception
     * @see {@link Method#invoke(Object, Object...)}
     * @throws InvocationTargetException underlying exception
     * @see {@link Method#invoke(Object, Object...)}
     */
    public Object get(Object obj, String property) throws IllegalArgumentException, IllegalAccessException,
            InvocationTargetException {
        if (getters.containsKey(property)) {
            NestedHandler member = getters.get(property);
            return member.get(obj);
        } else {
            NestedHandler nestedHandler = new NestedHandler(property, false);
            getters.put(property, nestedHandler);
            return nestedHandler.get(obj);
        }

    }

    /**
     * goes through fields and initialize caches setters and getters.
     * @param initLazy is lazy
     */
    private void init(boolean initLazy) {
        if (initLazy)
            return;
        Field[] fields = adaptedClass.getFields();

        for (Field field : fields) {
            field.setAccessible(true);
            setters.put(field.getName(), new NestedHandler(field));
            getters.put(field.getName(), new NestedHandler(field));
        }
        Method[] methods = adaptedClass.getMethods();
        for (Method method : methods) {

            if (isPropertyAccessor(method))
                continue;// not a property accessor;
            String propertyName = getPropertyAccessor(method);
            if (propertyName != null)
                getters.put(propertyName, new NestedHandler(method));
            propertyName = setPropertyAccessor(method);
            if (propertyName != null)
                setters.put(propertyName, new NestedHandler(method));
        }

    }

    /**
     * return a property name for setter methods .
     * @param method method
     * @return null if not starts with set
     */
    private String setPropertyAccessor(Method method) {
        return propertyAccessor(method, SET);
    }

    /**
     * return a property name for getter methods .
     * @param method method
     * @return null if not starts with is or get
     */
    private String getPropertyAccessor(Method method) {
        return propertyAccessor(method, IS, GET);
    }

    /**
     * check if the method starts with given prefix and if yes retriev the
     * property name.
     * @param method method
     * @param prefixs potential prefix s
     * @return null is not available
     */
    private String propertyAccessor(Method method, String... prefixs) {
        String name = method.getName();
        String property;
        String prefix = null;
        for (String prefixCandidate : prefixs) {
            if (name.startsWith(prefixCandidate)) {
                prefix = prefixCandidate;
                break;
            }
        }

        // if not qualified accessor return null
        if (prefix == null)
            return null;
        if (name.length() < prefix.length() + 1)
            return null;
        property = name.substring(prefix.length(), prefix.length() + 1).toLowerCase(Locale.ENGLISH);
        if (name.length() > prefix.length() + 2)
            property += name.substring(prefix.length() + 1);

        return property;
    }

    /**
     * checks if there is any valid prefix: is,get,get.
     * @param method method to be check
     * @return true if there is any parameter
     */
    private boolean isPropertyAccessor(Method method) {
        String name = method.getName();
        return name.startsWith(IS) || name.startsWith(GET) || name.startsWith(SET);
    }

    /**
     * property handle property.
     * @author altuure
     */
    public class NestedHandler {
        /**
         * list of access members
         */
        private List<Member> members;
        private List<Member> setMembers;

        /**
         * single method handler.
        * @param member delegated member
         */
        public NestedHandler(Member member) {
            members = new ArrayList<Member>(1);
            members.add(member);

        }

        /**
         * create instance for given class for given property.
         * @param property property eg: name or product.name
         * @param writeOperation true if set else false
         */
        public NestedHandler(String property, boolean writeOperation) {
            initThis(writeOperation, property);
        }

        /**
         * parse property and find access method.
         * @param writeOperation true if set else false
         * @param property property
         */
        private void initThis(boolean writeOperation, String property) {
            String[] split = Utils.splitDot(property);
            members = new ArrayList<Member>(split.length - 1);
            setMembers = new ArrayList<Member>(split.length - 1);
            Class currentClass = adaptedClass;

            for (int i = 0; i < split.length - 1; i++) {
                String string = split[i];
                Member member;
                member = findAccessor(currentClass, i, string, GET, IS);
                Member setMember = findAccessor(currentClass, i, string, SET);
                if (member == null)
                    throw new IllegalArgumentException("no such property found " + currentClass.getName() + "."
                            + string);
                members.add(member);
                setMembers.add(setMember);
                if (member instanceof Method) {
                    Method m = (Method) member;
                    currentClass = m.getReturnType();
                } else if (member instanceof Field) {
                    Field m = (Field) member;
                    currentClass = m.getType();
                }
            }
            Member lastMember;
            String lastProperty = split[split.length - 1];
            if (writeOperation) {
                lastMember = findAccessor(currentClass, split.length - 1, lastProperty, SET);
            } else {
                lastMember = findAccessor(currentClass, split.length - 1, lastProperty, GET, IS);
            }
            if (lastMember == null)
                throw new IllegalArgumentException("no such property found " + currentClass.getName() + "."
                        + lastProperty);
            members.add(lastMember);

        }

        /**
         * find access method or field for given class.
         * @param currentClass given class
         * @param i depth
         * @param property property with no .
         * @param prefixs possible method prefixs
         * @return null if not found
         */
        @SuppressWarnings("unchecked")
        private Member findAccessor(Class currentClass, int i, String property, String... prefixs) {
            Member member = null;
            if (i == 0 && getters.containsKey(property)) {
                member = getters.get(property).getLastMember();
            } else {

                for (String prefix : prefixs) {
                    try {
                        member = findMethod(currentClass, property, prefix);
                        break;
                    } catch (NoSuchMethodException e) {
                        if (log.isDebugEnabled())
                            log.debug("method not found: " + currentClass.getName() + "#" + prefix
                                    + Utils.capitilize(property));

                    } catch (SecurityException e) {
                        if (log.isDebugEnabled())
                            log.debug("method is not accessible: " + currentClass.getName() + "#" + prefix
                                    + Utils.capitilize(property));
                    }

                }

                try {
                    if (member == null) {
                        final Field field = currentClass.getField(property);
                        field.setAccessible(true);
                        member = field;
                    }

                } catch (NoSuchFieldException e) {
                    if (log.isDebugEnabled())
                        log.debug("field is not found: " + currentClass.getName() + "#" + property);

                }
            }
            return member;
        }

        private Method findMethod(Class currentClass, String property, String prefix) throws NoSuchMethodException{
            String name = prefix + Utils.capitilize(property);
            
            try {
                return currentClass.getMethod(name);
            } catch (NoSuchMethodException e) {
               log.debug("method not found",e);
            }
            Method[] methods = currentClass.getMethods();
            for (Method method : methods) {
                if(method.getName().equals(name))
                    return method;
            }
            throw new NoSuchMethodException("no such method found in "+currentClass.getName()+":"+name);
        }

        /**
         * get object property value.
         * @param obj object
         * @return value
         * @throws IllegalArgumentException underlying exception
         * @see {@link Method#invoke(Object, Object...)}
         * @throws IllegalAccessException underlying exception
         * @see {@link Method#invoke(Object, Object...)}
         * @throws InvocationTargetException underlying exception
         * @see {@link Method#invoke(Object, Object...)}
         */
        public Object get(Object obj) throws IllegalArgumentException, IllegalAccessException,
                InvocationTargetException {
            Object currentObject = obj;
            for (Member member : members) {
                currentObject = executeGetterMember(currentObject, member);
            }
            return currentObject;
        }

        /**
         * set object property value.
         * @param obj object
         * @param value property value
         * @throws IllegalArgumentException underlying exception
         * @see {@link Method#invoke(Object, Object...)}
         * @throws IllegalAccessException underlying exception
         * @see {@link Method#invoke(Object, Object...)}
         * @throws InvocationTargetException underlying exception
         * @throws InstantiationException new instance error
         * @see {@link Method#invoke(Object, Object...)}
         */
        public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException,
                InvocationTargetException, InstantiationException {
            Object currentObject = obj;
            for (int i = 0; i < members.size(); i++) {
                Member member = members.get(i); 
        
                if (i < members.size()-1) {
                    Object tempObject = executeGetterMember(currentObject, member);
                    if(tempObject==null){
                        Member setMember = setMembers.get(i);
                        tempObject=initSetterMember(currentObject, setMember);
                    }
                    currentObject=tempObject;
                } else {
                    currentObject = executeSetterMember(currentObject, member, value);

                }
            }
        }

        /**
         * execute setter method or field.
         * @param currentObject obj
         * @param member member
         * @param value set value
         * @return get value
         * @throws IllegalAccessException delegated exception
         * @throws java.lang.reflect.InvocationTargetException delegated exception
         */
        private Object executeSetterMember(Object currentObject, Member member, Object value)
                throws IllegalAccessException, InvocationTargetException {
            if (member instanceof Method) {
                Method m = (Method) member;
                currentObject = m.invoke(currentObject, value);
            } else if (member instanceof Field) {
                Field m = (Field) member;
                m.set(currentObject, value);
                currentObject = null;
            }
            return currentObject;
        }

        private Object initSetterMember(Object currentObject, Member member) throws IllegalAccessException,
                InvocationTargetException, InstantiationException {
            Object value = null;
            if (member instanceof Method) {
                Method m = (Method) member;
                value = m.getParameterTypes()[0].newInstance();
                m.invoke(currentObject, value);
            } else if (member instanceof Field) {
                Field m = (Field) member;
                value = m.getType().newInstance();
                m.set(currentObject, value);

            }
            return value;
        }

        /**
         * Execute getter method or field.
         * @param currentObject obj
         * @param member executed value
         * @return return value
         * @throws IllegalAccessException delegated exception
         * @throws java.lang.reflect.InvocationTargetException delegated exception
         */
        private Object executeGetterMember(Object currentObject, Member member) throws IllegalAccessException,
                InvocationTargetException {
            if (member instanceof Method) {
                Method m = (Method) member;
                currentObject = m.invoke(currentObject);
            } else if (member instanceof Field) {
                Field m = (Field) member;
                currentObject = m.get(currentObject);
            }
            return currentObject;
        }
        

        /**
         * last property field or method.
         * @return member
         */
        public Member getLastMember() {
            return members.get(members.size() - 1);
        }

    }

}
